/*
 * GeoVISTA Center (Penn State, Dept. of Geography)
 * Copyright (c), 2003-2011,  Jin Chen and Alan M. MacEachren, GeoVISTA Center, Penn State University
 * All Rights Researved.
 *
 * This library is developed and distributed for free usage,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * You can redistribute and/or modify it under the terms of the
 * Simplified BSD License
 *
 *
 *
 *
 *
 *
 *
 *
 * @author: jin Chen 
 * @date: Sep 15, 2003$
 * @version: 1.0
 */
package edu.psu.geovista.app.pcp.toolkits.Math;

import edu.psu.geovista.data.model.AppDataModel;

import java.math.BigDecimal;
import java.util.Arrays;

public class JMath {
    public static final int LABEL_x1=1;//*1
    public static final int LABEL_xK=2;//*1000
    public static final int LABEL_xM=3;//*1,000,000
    public static final int FORMAT_DIGIT=1;//float or double
    public static final int FORMAT_INT=2;
    public static final int FORMAT_DATE=3;


    /*******************************************************************************************************
     *                Median
     *******************************************************************************************************/
    /**
     *
     * @param data
     * @return  median value of data array. If data contain only invalid value(e.g. NaN or -Integer.Max) values, return NaN
     */
    public static float calculateMedian(float[] data) {
       if(data==null){
           throw new NullPointerException("Unable to calculate null value");
       }
       if(data.length <=0){
           throw new IllegalArgumentException("unable to calculate on 0-length array");
       }
       //Arrays.sort(data);
       float[] newData=getValidData(data);//remove invalid data
       if(newData.length ==0){
           //the data contain only invalid data, it median value =NaN
           return Float.NaN ;
       }
       return calculateMedianForSortValue(newData);
    }
    /**
     *
     * @param data  must be a sort value.
     * @return
     */
    public static float calculateMedianForSortValue(float[] data) {

        int[] medianIndexs=getMedianIndex(0,data.length -1);
        return calculateIndexValue(data,medianIndexs);
    }
    /*******************************************************************************************************
     *                For calculate boxplot
     *******************************************************************************************************/
    public static float calculateLowerQuartile(float[] data) {
         Arrays.sort(data);
         int[] medianIndexs=getMedianIndex(0,data.length -1);
         int[] lqIndexs=getMedianIndex(0,medianIndexs[0]);
         return calculateIndexValue(data,lqIndexs);
         /*if (medianIndexs[1]==-1){//odd
            int[] lqIndexs=getMedianIndex(0,medianIndexs[0]);

        }
        else{//even
            int[] lqIndexs=getMedianIndex(0,medianIndexs[0]);
            return(data[medianIndexs[0]]+data[medianIndexs[1]])/2;

        }*/
    }
    public static float calculateUpperQuartile(float[] data) {
        Arrays.sort(data);
        int[] medianIndexs=getMedianIndex(0,data.length -1);
         if (medianIndexs[1]==-1){//odd
            int[] uqIndexs=getMedianIndex(medianIndexs[0],data.length -1);
            return calculateIndexValue(data,uqIndexs);

        }
        else{//even
            int[] uqIndexs=getMedianIndex(medianIndexs[1],data.length -1);
            return calculateIndexValue(data,uqIndexs);

        }
    }
    /**
     * Given a data array, return the value of element with specified index. If index has 2 elements,
     * return average value of elements with the indexs
     * @param data
     * @param indexs
     * @return
     */
    private static float calculateIndexValue(float[] data, int[] indexs){

        if (indexs[1]==-1){//odd
            return data[indexs[0]];
        }
        else{//even
            return(data[indexs[0]]+data[indexs[1]])/2;

        }
    }
    /**
     *  si,ei : start index, end index
     *  return int[]  median index
     */
    private static int[] getMedianIndex(int si,int ei) {
        int index[]=new int[2];
        int nCells=Math.abs(si-ei)+1;
        int middle=0;
        if (nCells%2==0){//even number
            middle=(si+ei)/2;
            index[0]=middle;
            index[1]=middle+1;

        }
        else{//odd
            middle=(si+ei)/2;
            index[0]=middle;
            index[1]=-1;//means no index available
        }
        return index;

    }
    /**
        * Assume AppDataModel.NULL_INT=Integer.MIN_VALUE
        * @param a
        * @return   an array that contain neither NaN or AppDataModel.NULL_INT  value
        */
       public static float[] getValidData(float[] a){
           Arrays.sort(a);
           int lastIntNullIndex=-1;
           int firstNaNIndex=-1;
           int start=0; // 
           int end=a.length -1;
           int len=0;
           for (int i=0;i<a.length ;i++){
               if(a[i]==AppDataModel.NULL_INT){//
                   lastIntNullIndex=i;

               }

               if(Float.isNaN(a[i]) ){
                   //For Arrays.sort(a), NaN is considered greater than any other floating-point value
                   firstNaNIndex=i;
                   break;
               }

           }
           if(lastIntNullIndex!=-1) //there is int_null data
               start=lastIntNullIndex+1;
           if(firstNaNIndex!=-1) //there is NaN data
               end=firstNaNIndex-1;
           len=end-start+1;
           float[] r=new float[len];
           System.arraycopy(a,start,r,0,len);
           return r;
       }
       /**
        *
        * @param values
        * @return   max value of the array
        */
       public static float getMax(float[] values){
            float[] v=new float[values.length ];
            System.arraycopy(values,0,v,0,values.length );
            Arrays.sort(v);
            return v[v.length -1];

       }

        /**
        *
        * @param values
        * @return   max value of the array
        */
       public static float getMin(float[] values){
            float[] v=new float[values.length ];
            System.arraycopy(values,0,v,0,values.length );
            Arrays.sort(v);
            return v[0];

       }
    /**
     * only keep <numDigit> digit number
     * @param value
     * @param numDigit
     * @return
     */
    public  static String getRoundValue(float value, int numDigit){
        BigDecimal bd=new BigDecimal(value);
        BigDecimal r=bd.setScale(numDigit,BigDecimal.ROUND_DOWN );

        return r.toString() ;
    }
     /**
     * only keep <numDigit> digit number
     * @param value
     * @param numDigit
     * @return
     */
    public  static String getRoundValue(double value, int numDigit){
        BigDecimal bd=new BigDecimal(value);
        BigDecimal r=bd.setScale(numDigit,BigDecimal.ROUND_DOWN );

        return r.toString() ;
    }

    public static String getScaledValue(float tickValue, int scale,int numOfDigit) {
          String stv=null;
          if(scale ==LABEL_xK){
                        stv=getRoundValue(tickValue/1000,numOfDigit);
                        stv=stv+" K";
                        }
                    else if(scale==LABEL_xM){
                        stv=getRoundValue(tickValue/1000000,numOfDigit);
                        stv=stv+" M";
                    }
                    else if(scale ==LABEL_x1){
                        stv=getRoundValue(tickValue,numOfDigit);
                    }
            return stv;
    }
     /**
      *
      * @param tickValue        data value
      * @param scale            how u want to scale
      * @param numOfDigit       the number of digits
      * @param format           int, double or date....
      * @return
      */
     public static String getReadableValue(float tickValue, int scale,int numOfDigit,int format){
        if(format==FORMAT_INT){
            return JMath.getScaledValue(tickValue,scale ,0);

        }
        else if(format ==FORMAT_DIGIT){
            return JMath.getScaledValue(tickValue,scale ,numOfDigit);
        }
        else{//for date not implmenent yet
           return JMath.getScaledValue(tickValue,scale ,0);
        }


    }

}
